/**
 * Copyright (C) 2010-2011 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.googlecode.flyway.core.dbsupport.h2;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;

import com.googlecode.flyway.core.dbsupport.DbSupport;
import com.googlecode.flyway.core.migration.sql.PlaceholderReplacer;
import com.googlecode.flyway.core.migration.sql.SqlScript;
import com.googlecode.flyway.core.migration.sql.SqlStatement;

/**
 * H2 database specific support
 */
public class H2DbSupport implements DbSupport {
	/**
	 * The jdbcTemplate to use.
	 */
	private final JdbcTemplate jdbcTemplate;

	/**
	 * Creates a new instance.
	 * 
	 * @param jdbcTemplate
	 *            The jdbcTemplate to use.
	 */
	public H2DbSupport(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	@Override
	public String getScriptLocation() {
		return "com/googlecode/flyway/core/dbsupport/h2/";
	}

	@Override
	public String getCurrentUserFunction() {
		return "USER()";
	}

	@Override
	public String getCurrentSchema() {
		return (String) jdbcTemplate.execute(new ConnectionCallback() {
			@Override
			public String doInConnection(Connection connection) throws SQLException, DataAccessException {
				ResultSet resultSet = connection.getMetaData().getSchemas();
				while (resultSet.next()) {
					if (resultSet.getBoolean("IS_DEFAULT")) {
						return resultSet.getString("TABLE_SCHEM");
					}
				}
				return null;
			}
		});
	}

	@Override
	public boolean isSchemaEmpty() {
		String[] schemas = getMupSchemas();
		for (String schema : schemas) {
			@SuppressWarnings({ "unchecked" })
			List<Map<String, Object>> tables = jdbcTemplate.queryForList("SHOW TABLES FROM " + schema);
			if (!tables.isEmpty()) {
				return false;
			}
		}
		return true;
	}

	@Override
	public boolean tableExists(final String table) {
		return (Boolean) jdbcTemplate.execute(new ConnectionCallback() {
			@Override
			public Boolean doInConnection(Connection connection) throws SQLException, DataAccessException {
				ResultSet resultSet = connection.getMetaData().getTables(null, getCurrentSchema(), table.toUpperCase(),
						null);
				return resultSet.next();
			}
		});
	}

	@Override
	public boolean columnExists(final String table, final String column) {
		return (Boolean) jdbcTemplate.execute(new ConnectionCallback() {
			@Override
			public Boolean doInConnection(Connection connection) throws SQLException, DataAccessException {
				ResultSet resultSet = connection.getMetaData().getColumns(null, getCurrentSchema(),
						table.toUpperCase(), column.toUpperCase());
				return resultSet.next();
			}
		});
	}

	@Override
	public boolean supportsDdlTransactions() {
		return false;
	}

	@Override
	public void lockTable(String table) {
		jdbcTemplate.execute("select * from " + table + " for update");
	}

	@Override
	public String getBooleanTrue() {
		return "1";
	}

	@Override
	public String getBooleanFalse() {
		return "0";
	}

	@Override
	public SqlScript createSqlScript(String sqlScriptSource, PlaceholderReplacer placeholderReplacer) {
		return new H2SqlScript(sqlScriptSource, placeholderReplacer);
	}

	/**
	 * FIXME ATTENTION: this is weltbild specific!
	 */
	private String[] getMupSchemas() {
		return new String[] { getCurrentSchema(), "CORE", "EXPORT", "IMPORT", "NORMALIZATION", "SMS", "CMS",
				"SHOPADMIN" };
	}

	@Override
	public SqlScript createCleanScript() {
		String[] mupSchemas = getMupSchemas();
		List<SqlStatement> sqlStatements = new ArrayList<SqlStatement>();
		for (String schema : mupSchemas) {
			sqlStatements.addAll(generateDropTableStatements(schema));
			sqlStatements.addAll(generateDropStatements("SEQUENCE", schema, sqlStatements.size()));
			sqlStatements.addAll(generateDropStatements("CONSTANT", schema, sqlStatements.size()));
			sqlStatements.addAll(generateDropStatements("DOMAIN", schema, sqlStatements.size()));
		}
		return new SqlScript(sqlStatements);
	}

	/**
	 * Generate the statements for dropping all the tables in the current
	 * schema.
	 * 
	 * @return The list of statements.
	 */
	private List<SqlStatement> generateDropTableStatements(String schema) {
		@SuppressWarnings({ "unchecked" })
		List<Map<String, Object>> tables = jdbcTemplate.queryForList("SHOW TABLES FROM " + schema);

		List<String> viewNames = getViewNames(schema);

		LinkedList<SqlStatement> sqlStatements = new LinkedList<SqlStatement>();
		int count = 0;
		for (Map<String, Object> table : tables) {
			count++;
			final Object tableName = table.get("TABLE_NAME");
			if (!viewNames.contains(tableName)) {
				sqlStatements
						.add(new SqlStatement(count, "DROP TABLE \"" + schema + "\".\"" + tableName + "\" CASCADE"));
			} else {
				sqlStatements.addFirst(new SqlStatement(count, "DROP VIEW \"" + schema + "\".\"" + tableName + "\""));
			}
		}
		return sqlStatements;
	}

	private List<String> getViewNames(String schema) {
		@SuppressWarnings("unchecked")
		List<String> viewNames = jdbcTemplate.queryForList(
				"SELECT TABLE_NAME from INFORMATION_SCHEMA.VIEWS where TABLE_SCHEMA=?", new Object[] { schema },
				String.class);
		return viewNames;

	}

	/**
	 * Generate the statements for dropping all the objects of this type in the
	 * current schema.
	 * 
	 * @param objectType
	 *            The type of object to drop (Sequence, constant, ...)
	 * @param initialLineNumber
	 *            The initial line number of the first statement that will be
	 *            added to the drop script.
	 * 
	 * @return The list of statements.
	 */
	private List<SqlStatement> generateDropStatements(String objectType, String schema, int initialLineNumber) {
		@SuppressWarnings({ "unchecked" })
		List<Map<String, Object>> objectNames = jdbcTemplate.queryForList("SELECT " + objectType
				+ "_NAME FROM information_schema." + objectType + "s WHERE " + objectType + "_schema = ?",
				new Object[] { schema });

		List<SqlStatement> sqlStatements = new ArrayList<SqlStatement>();
		int count = initialLineNumber;
		for (Map<String, Object> objectName : objectNames) {
			sqlStatements.add(new SqlStatement(count, "DROP " + objectType + " IF EXISTS \"" + schema + "\".\""
					+ objectName.get(objectType + "_NAME") + "\""));
			count++;
		}
		return sqlStatements;
	}
}